﻿package no.doomsday.aronning.engines.verlets 
{
	import flash.display.Graphics;
	import flash.events.EventDispatcher;
	import flash.geom.Rectangle;
	import no.doomsday.aronning.engines.verlets.constraints.AbstractConstraint;
	import no.doomsday.aronning.engines.verlets.constraints.IConstraint;
	import no.doomsday.aronning.engines.verlets.constraints.PointConstraint;
	import no.doomsday.aronning.engines.verlets.constraints.RectangleConstraint;
	import no.doomsday.aronning.engines.verlets.constraints.StickConstraint;
	import no.doomsday.aronning.engines.verlets.forces.ConstantForce;
	import no.doomsday.aronning.utilities.parameters.Parameter;
	import no.doomsday.aronning.engines.verlets.geometry.Cloth;
	import no.doomsday.aronning.engines.verlets.geometry.Hull;
	import no.doomsday.aronning.engines.verlets.utils.CellSpaceManager;
	import no.doomsday.aronning.engines.verlets.utils.LinkedListNode;
	
	/**
	 * ...
	 * @author Andreas Rønning
	 */
	public class VerletSystem extends EventDispatcher
	{		
		protected var accumulator:ConstantForce = new ConstantForce(0, 0);
		
		public var startVerlet:Verlet = new Verlet();
		public var startConstraint:PointConstraint = new PointConstraint();
		public var startStick:PointConstraint = new PointConstraint();
		public var startForce:ConstantForce = new ConstantForce();
		private var cellspaceManager:CellSpaceManager = new CellSpaceManager();
		private var useCellSpacePartitioning:Boolean = false;
		public var selfClean:Boolean = false;
		/**
		 * A manager class for handling verlet systems, constraints and updates
		 */
		public function VerletSystem() 
		{
			
		}
		/**
		 * Removes all forces, verlets and constraints
		 */
		public function reset():void {
			startVerlet = new Verlet();
			startConstraint = new PointConstraint();
			startStick = new PointConstraint();
			startForce = new ConstantForce();
			removeAllConstraints();
			Hull.cleanUp();
		}
		/**
		 * Removes a constraint
		 * @param	constraint
		 * The constraint to remove
		 * @return
		 * Wether a constraint was found and removed or not
		 */
		public function removeConstraint(constraint:AbstractConstraint):Boolean {
			try {
				constraint.destroy();
				return true;
			}catch(e:Error){}
			return false;
		}
		/**
		 * Adds a rectangle constraint. Specified verlets cannot leave this rectangle
		 * @param	dim
		 * The rectangle space to constrain to
		 * @param	targetAll
		 * Wether the constraint should be applied to all verlets in the system
		 * @param	...targetPoints
		 * Verlets to constrain
		 * @return
		 * An instance of RectangleConstraint
		 */
		public function addRectangleConstraint(dim:Rectangle, targetAll:Boolean, friction:Parameter, ...targetPoints:Array):RectangleConstraint {
			var constrainedVerlets:Vector.<Verlet> = new Vector.<Verlet>();
			var i:int;
			var rectConstraint:RectangleConstraint = new RectangleConstraint(dim,this,targetAll, friction);
			if (!targetAll){
				for (i = 0; i < targetPoints.length; i++) 
				{
					constrainedVerlets.push(targetPoints[i]);
				}
				rectConstraint.constrainedVerlets = constrainedVerlets;
			}
			startConstraint.append(rectConstraint);
			return rectConstraint;
		}
		/**
		 * Adds a constant force to the system
		 * @param	x
		 * The directional pull of the force along the X vector
		 * @param	y
		 * The directional pull of the force along the Y vector
		 * @return
		 * An instance of ConstantForce
		 */
		public function addForce(x:Number, y:Number):ConstantForce {
			var f:ConstantForce = new ConstantForce(x, y);
			startForce.append(f);
			return f;
		}
		/**
		 * Adds a point constraint. The targeted verlets will ALWAYS be locked to this position
		 * @param	x
		 * The x position to constrain to
		 * @param	y
		 * The y position to constrain to
		 * @param	...targetPoints
		 * The verlets to constrain
		 * @return
		 * An instance of PointConstraint
		 */
		public function addPointConstraint(x:Number,y:Number,...targetPoints:Array):PointConstraint {
			var pointConstraint:PointConstraint = new PointConstraint(x, y);
			var v:Vector.<Verlet> = new Vector.<Verlet>();
			for (var i:int = 0; i < targetPoints.length; i++) 
			{
				v.push(targetPoints[i]);
			}
			pointConstraint.constrainedVerlets = v;
			startConstraint.append(pointConstraint);
			return pointConstraint;
		}
		/**
		 * Adds a stick constraint of variable stiffness. 
		 * @param	p1
		 * The first verlet to constrain
		 * @param	p2
		 * The second verlet to constrain
		 * @param	stiffness
		 * The stiffness. An instance of the verlets.utils.Parameter class
		 * @param	autoRestLength
		 * Wether the restlength should be automatically computed based on the point distances
		 * @param	restLength
		 * The distance the verlets will try to reach within one another
		 * @return
		 * An instance of the StickConstraint class
		 */
		public function addStickConstraint(p1:Verlet, p2:Verlet, stiffness:Parameter = null, autoRestLength:Boolean = true, restLength:Number = 40, fastMode:Boolean = false):StickConstraint {
			var stick:StickConstraint = new StickConstraint(p1, p2, stiffness, autoRestLength, restLength, fastMode);
			startStick.append(stick);
			return stick;
		}
		/**
		 * Adds a verlet to the system
		 * @param	x
		 * The initial x position
		 * @param	y
		 * The initial y position
		 * @return
		 * An instance of the Verlet class
		 */
		public function addPoint(x:Number, y:Number):Verlet {
			var v:Verlet = new Verlet(x, y);
			startVerlet.append(v);
			return v;
		}
		/**
		 * Removes the verlet. If it is part of any stick constraints, those constraints are destroyed.
		 * @param	v
		 * The verlet to remove
		 * @return
		 * The verlet that was removed
		 */
		public function removePoint(v:Verlet):Verlet {
			v.destroy();
			var c:LinkedListNode = startConstraint;
			while (c.next) {
				c = c.next;
				var idx:int = (c as AbstractConstraint).constrainedVerlets.indexOf(v);
				if (idx < 0) continue;
				(c as AbstractConstraint).constrainedVerlets.splice(idx, 1);
			}
			c = startStick;
			while (c.next) {
				c = c.next;
				idx = (c as AbstractConstraint).constrainedVerlets.indexOf(v);
				if (idx < 0) continue;
				removeConstraint(c as AbstractConstraint);
			}
			Hull.cleanUp();
			return v;
		}
		/**
		 * Updates the system based on a timestep.
		 * @param	timeStep
		 * Use a linear timestep (1/stage.frameRate for instance) for the most stable results
		 */
		public function update(timeStep:Number, iterations:uint = 5):void {
			accumulator.x = 0;
			accumulator.y = 0;
			
			var f:ConstantForce;
			var v:Verlet;
			var u:IUpdatable;
			var n:LinkedListNode;
			n = startForce;
			while (n.next) {
				n = n.next;
				f = n as ConstantForce;
				accumulator.x += f.x;
				accumulator.y += f.y;
			}
			//verlets
			n = startVerlet;
			while (n.next) {
				n = n.next;
				v = n as Verlet;
				v.update(timeStep, accumulator);
			}
			//other constraints
			n = startConstraint;
			while (n.next) {
				n = n.next;
				u = n as IUpdatable;
				u.update(timeStep);
			}
			//sticks
			for (var i:uint = iterations; i--;){
				n = startStick;
				while (n.next) {
					n = n.next;
					u = n as IUpdatable;
					u.update(timeStep);
				}
			}
			
		}
		/**
		 * Renders points and constraints to the specified graphics object
		 * @param	g
		 * The graphics object to draw in
		 */
		public function render(g:Graphics):void {
			var i:int;
			var r:IRenderable;
			var n:LinkedListNode = startVerlet;
			var v:Verlet;
			while (n.next) {
				n = n.next;
				r = n as IRenderable;
				r.render(g);
			}
			n = startConstraint;
			while (n.next) {
				n = n.next;
				r = n as IRenderable;
				r.render(g);
			}
			
			n = startStick;
			while (n.next) {
				n = n.next;
				r = n as IRenderable;
				r.render(g);
			}
			
		}
		/**
		 * Creates a basic cloth made up from a grid of sticks and verlets
		 * @param	dim
		 * The size of the cloth
		 * @param	stiffness
		 * The stiffness of the sticks
		 * @param	divisionsWidth
		 * The number of horizontal squares
		 * @param	divisionsHeight
		 * The number of vertical squares
		 * @param	fast
		 * Wether the constraints will use fast Math.sqrt approximation or not (slightly less stable)
		 * @return
		 * An instance of Cloth
		 */
		public function createCloth(dim:Rectangle, stiffness:Parameter, divisionsWidth:uint = 10, divisionsHeight:uint = 10, fast:Boolean = false):Cloth {
			var cloth:Cloth = new Cloth();
			var corners:Vector.<Verlet> = new Vector.<Verlet>();
			var clothArray:Array = [];
			var verletList:Vector.<Verlet> = new Vector.<Verlet>();
			var stickList:Vector.<StickConstraint> = new Vector.<StickConstraint>();
			var Y:uint = divisionsHeight;
			var X:uint = divisionsWidth;
			for (var y:int = 0; y < divisionsHeight; y++ ) {
				clothArray.push([]);
				for (var x:int = 0; x < divisionsWidth; x++ ) {
					var v:Verlet = addPoint(x * dim.width/divisionsWidth, y * dim.height/divisionsHeight);
					clothArray[y].push(v);
					verletList.push(v);
				}
			}
			corners.push(clothArray[0][0]);
			corners.push(clothArray[0][divisionsWidth-1]);
			corners.push(clothArray[divisionsHeight-1][divisionsWidth-1]);
			corners.push(clothArray[divisionsHeight-1][0]);
						
			for (y = 0; y < clothArray.length; y++) {
				for (x = 0; x < clothArray[y].length; x++) {
					if (y > 0) {
						stickList.push(addStickConstraint(clothArray[y][x], clothArray[y - 1][x], stiffness, true,0,fast));
					}
					if (x > 0) {
						stickList.push(addStickConstraint(clothArray[y][x - 1], clothArray[y][x], stiffness, true,0,fast));
					}
				}
			}
			
			cloth.corners = corners;
			cloth.points = verletList;
			cloth.stiffness = stiffness;
			cloth.sticks = stickList;
			return cloth;
		}
		/**
		 * Creates a box of verlets and sticks
		 * @param	dim
		 * The size and position of the box
		 * @param	stiffness
		 * The stiffness of the sticks
		 * @return
		 * A vector of the four corner verlets in clockwise order (useful for constraints)
		 */
		public function createBox(dim:Rectangle,stiffness:Parameter):Vector.<Verlet> {
			//var centerX:Number = dim.x+dim.width / 2;
			//var centerY:Number = dim.y + dim.height / 2;
			
			var tl:Verlet = addPoint(dim.x+0, dim.y+0);
			var tr:Verlet = addPoint(dim.x+dim.width, dim.y+0);
			var bl:Verlet = addPoint(dim.x+0, dim.y+dim.height);
			var br:Verlet = addPoint(dim.x + dim.width, dim.y + dim.height);
			
			addStickConstraint(tl, tr,stiffness);
			addStickConstraint(tr, br,stiffness);
			addStickConstraint(br, bl,stiffness);
			addStickConstraint(bl, tl,stiffness);
			addStickConstraint(bl, tr,stiffness);
			addStickConstraint(tl, br, stiffness);
			
			var createdVerlets:Vector.<Verlet> = new Vector.<Verlet>();
			createdVerlets.push(tl);
			createdVerlets.push(tr);
			createdVerlets.push(bl);
			createdVerlets.push(br);
			return createdVerlets;
		}
		/**
		 * Get the closest verlet to a point
		 * @param	x
		 * @param	y
		 * @return
		 * The closest verlet
		 */
		public function getClosestVerletToPoint(x:Number, y:Number):Verlet {
			//TODO Use CSP to get the closest verlet
			if (!startVerlet.next) throw new Error("No verlets in system");
			var closestVerletSoFar:Verlet;
			var closestDistanceSoFar:Number = Number.POSITIVE_INFINITY;
			if (!useCellSpacePartitioning) {
				var v:LinkedListNode = startVerlet;
				while (v.next) {
					v = v.next;
					var verlet:Verlet = v as Verlet;
					var diffX:Number = x-verlet.x;
					var diffY:Number = y-verlet.y;
					var dist:Number = Math.sqrt(diffX * diffX + diffY * diffY);
					//trace(dist);
					if (dist <= closestDistanceSoFar) {
						closestVerletSoFar = verlet;
						closestDistanceSoFar = dist;
					}
				}
			}
			return closestVerletSoFar;
		}
		public function getConvexHull(verlets:Vector.<Verlet>):Hull {
			return new Hull(verlets);
		}
		public function removeEmptyConstraints():void {
			var n:LinkedListNode = startConstraint;
			while (n.next) {
				n = n.next;
				if (n is RectangleConstraint) {
					if((n as RectangleConstraint).constrainAll)	continue;
				}
				if ((n as AbstractConstraint).constrainedVerlets.length == 0) {
					n.destroy();
				}
			}
		}
		
		public function removeAllConstraints():void {
			var n:LinkedListNode = startConstraint;
			while (n.next) {
				n = n.next;
				n.destroy();
			}
			n = startStick;
			while (n.next) {
				n = n.next;
				n.destroy();
			}
		}
		
	}
	
}